Skip to content

Commit

Permalink
Merge branch 'release/1.3.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
dotchetter committed Dec 26, 2023
2 parents 26bb610 + 30fd605 commit 3022718
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 12 deletions.
63 changes: 63 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
# Pyttman Changelog

V 1.3.2

### :star2: News* **
* **Removed clutter from log entries**

The log entries from Pyttman are now cleaner, without as much clutter for each log entry.
* **New argment to `EntityField` classes available: `post_processor`**

The `post_processor` argument allows you to define a function which will be called on the value of the entity after it has been parsed. This is useful for scenarios where you want to clean up the value of the entity, or perform other operations on it before it is stored in `message.entities` in the `respond` method.

```python
class SomeIntent(Intent):
"""
In this example, the name will be stripped of any leading or trailing whitespace.
"""
name = StringEntityField(default="", post_processor=lambda x: x.strip())
```
* **All `ability` classes are now available by exact name on the `app` instance**

The `app` instance in Pyttman apps now has all `Ability` classes available by their exact name, as defined in the `settings.py` file. This is useful for scenarios where you want to access the `storage` object of an ability, or other properties of the ability.

```python
# ability.py
class SomeAbility(Ability):
pass

# settings.py
ABILITIES = [
"some_ability.SomeAbility"
]

# any file in the project
from pyttman import app

```


### **🐛 Splatted bugs and corrected issues**

* **Fixed a bug where `LOG_TO_STDOUT` didn't work, and logs were not written to STDOUT:** [#86](https://github.com/dotchetter/Pyttman/issues/86)

# V 1.3.1

### :star2: News
* **New setting variable: `STATIC_FILES_DIR`**

This new setting is set by default in all new apps, and offers a standard way to keep static files in a project.
All new apps, even ones created with older versions of Pyttman, will have the `static_files` directory
as part of the app catalog.

* **Simplified the use of the logger in Pyttman**
The logger in pyttman offers a simple, ready-to-use logger for your app.
It offers a decorator previously as `@pyttman.logger.logged_method` which is now simplified to `@pyttman.logger`.


### **🐛 Splatted bugs and corrected issues**

* **Corrected an issue when using `pyttman runfile` to execute scripts**
An issue with the relative path to the script file being exeucted has been
corrected; now, an absolute path can be provided to the script file, and
the script will be executed as expected.
**

# V 1.3.0.1
Hotfix release, addressing an issue with PyttmanCLI executing scripts,
where the directory of the app is included in the path for a script
Expand Down
27 changes: 27 additions & 0 deletions devtools/upload_pypi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import shutil
import subprocess
from pathlib import Path

from setuptools import setup
from twine.commands.upload import upload

# Replace these with your package information
package_name = "Pyttman"

# Get the package version dynamically
# the file location is in a sibling directory, add it to the path

# Upload to PyPI using twine
confirm = input("Deploying to PyPi.\n\n1: For pypi production, type 'production'"
"\n2: For test.pypi.org, type 'test'\n\n")

if not Path("dist").exists():
print("You need to build the package first. Run devtools/build.py.")
exit(0)

print("Enter '__token__' for username, and the token for password")
if confirm == "production":
subprocess.run(["twine", "upload", "dist/*"])
elif confirm == "test":
subprocess.run(["twine", "upload", "--repository-url", "https://test.pypi.org/legacy/", "dist/*"])

31 changes: 30 additions & 1 deletion pyttman/core/entity_parsing/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,34 @@ class EntityFieldBase(EntityFieldValueParser, ABC):
Not only to find the word(s) but also type-convert to a given
datatype if a match is True.
"""
default = None
"""
Specify a default return value, if no match is found.
"""
type_cls = None
"""
The type_cls is the class which the value is converted to.
For example: int, str, float, etc.
"""
identifier_cls = None
"""
Identifier class is a class which specializes in finding
patterns in text. You can provide a custom Identifier class
to further increase the granularity of the value you're looking for.
"""
post_processor = None
"""
Pre-processor is a callable which is called before the
value is converted to the type_cls of the EntityField.
You can specify any callable here, and it will be called
with the value as its only argument.
"""

def __init__(self,
identifier: Type[Identifier] | None = None,
default: Any = None,
post_processor: callable = None,
**kwargs):
"""
:param as_list: If set to True combined with providing 'valid_strings',
Expand All @@ -48,6 +67,7 @@ def __init__(self,
You can read more about Identifier classes in the Pyttman
documentation.
"""
self.post_processor = post_processor
if self.type_cls is None or inspect.isclass(self.type_cls) is False:
raise InvalidPyttmanObjectException("All EntityField classes "
"must define a 'type_cls', "
Expand Down Expand Up @@ -91,6 +111,15 @@ def convert_value(self, value: Any) -> Any:
except Exception as e:
raise TypeConversionFailed(from_type=type(value),
to_type=self.type_cls) from e

try:
if self.post_processor is not None and callable(self.post_processor):
converted_value = self.post_processor(converted_value)
except Exception as e:
value_err = ValueError("The post_processor callable '"
f"'{self.post_processor}' failed: {e}")
raise TypeConversionFailed(from_type=type(value),
to_type=self.type_cls) from value_err
return converted_value

def before_conversion(self, value: Any) -> str:
Expand Down
24 changes: 21 additions & 3 deletions pyttman/core/entity_parsing/parsers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re
import string
import typing
from itertools import zip_longest
from typing import Type, Dict, Union
Expand All @@ -19,6 +21,8 @@ class EntityFieldValueParser(PrettyReprMixin):
EntityParser Api component: 'EntityField'.
"""
__repr_fields__ = ("identifier", "exclude", "prefixes", "suffixes")
ignore_chars = True
chars_to_ignore = ".,;:!?-"

def __init__(self,
prefixes: tuple | typing.Callable = None,
Expand Down Expand Up @@ -122,14 +126,21 @@ def parse_message(self,
output = []
word_index = 0

casefolded_msg = message.lowered_content()
message_lowered = message.lowered_content()
if self.ignore_chars:
# Strip away special chars
message_lowered = [
re.sub(rf"[{self.chars_to_ignore}]", "", word)
for word in message_lowered
]

common_occurrences = tuple(
OrderedSet(casefolded_msg).intersection(self.valid_strings))
OrderedSet(message_lowered).intersection(self.valid_strings))

for i, word in enumerate(common_occurrences):
if i > self.span and not self.as_list:
break
word_index = casefolded_msg.index(word)
word_index = message_lowered.index(word)
output.append(message.content[word_index])

if len(output) > 1:
Expand All @@ -142,6 +153,13 @@ def parse_message(self,
self._validate_prefixes_suffixes(message)

if self.value:
if self.ignore_chars:
if isinstance(self.value.value, list):
for i, elem in enumerate(self.value.value):
elem = re.sub(rf"[{self.chars_to_ignore}]", "", elem)
self.value.value[i] = elem
elif isinstance(self.value.value, str):
self.value.value = re.sub(rf"[{self.chars_to_ignore}]", "", self.value.value)
entity = self.value
if entity.value == self.default:
return
Expand Down
12 changes: 11 additions & 1 deletion pyttman/core/internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ class PyttmanApp(PrettyReprMixin):
client: Any
name: str | None = field(default=None)
settings: Settings | None = field(default=None)
abilities: set = field(default_factory=set)
hooks: LifecycleHookRepository = field(
default_factory=lambda: LifecycleHookRepository())
_abilities: set = field(default_factory=set)

def start(self):
"""
Expand All @@ -152,3 +152,13 @@ def start(self):
self.client.run_client()
except Exception:
warnings.warn(traceback.format_exc())

@property
def abilities(self):
return self._abilities

@abilities.setter
def abilities(self, abilities):
for ability in abilities:
setattr(self, ability.__class__.__name__, ability)
self._abilities.add(ability)
7 changes: 7 additions & 0 deletions pyttman/tools/pyttmancli/ability.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ def run_application(self) -> None:
# #(used for attribute access in completion)
app: PyttmanApp = None
if (app := self.storage.get("app")) is not None:
if not app.client.message_router.abilities:
print("There are no abilities loaded, the app will not "
"respond to any messages. Create abilities by "
"running 'pyttman new ability' in the terminal.\n"
"Next up, add them to your app's settings.py file "
"under the ABILITIES key.")
exit(0)
print(f"- Ability classes loaded: "
f"{app.client.message_router.abilities}")
app.start()
Expand Down
11 changes: 6 additions & 5 deletions pyttman/tools/pyttmancli/intents.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import code
import os
import re
import traceback
from pathlib import Path

Expand Down Expand Up @@ -28,7 +29,7 @@ class ShellMode(Intent, PyttmanCliComplainerMixin):
example = "pyttman shell <app name>"
help_string = "Opens a Python interactive shell with access to modules, " \
"app settings and the Pyttman 'app' object."
app_name = TextEntityField()
app_name = TextEntityField(default="")

def respond(self, message: Message) -> Reply | ReplyStream:
app_name = message.entities["app_name"]
Expand All @@ -48,7 +49,7 @@ class CreateNewApp(Intent, PyttmanCliComplainerMixin):
Create a new Pyttman app. The directory is terraformed
and prepared with a template project.
"""
app_name = TextEntityField()
app_name = TextEntityField(default="")
lead = ("new",)
trail = ("app",)
ordered = True
Expand Down Expand Up @@ -104,7 +105,7 @@ class RunAppInDevMode(Intent, PyttmanCliComplainerMixin):
Run a Pyttman app in dev mode. This sets "DEV_MODE"
to True and opens a chat interface in the terminal.
"""
app_name = TextEntityField()
app_name = TextEntityField(default="")
fail_gracefully = True
lead = ("dev",)
example = "pyttman dev <app name>"
Expand Down Expand Up @@ -147,7 +148,7 @@ class RunAppInClientMode(Intent, PyttmanCliComplainerMixin):
"settings.py under 'CLIENT'.\n" \
f"Example: {example}"

app_name = TextEntityField()
app_name = TextEntityField(default="")

def respond(self, message: Message) -> Reply | ReplyStream:
app_name = message.entities["app_name"]
Expand Down Expand Up @@ -177,7 +178,7 @@ class RunFile(Intent, PyttmanCliComplainerMixin):
help_string = "Run a singe file within a Pyttman app context. " \
f"Example: {example}"

app_name = TextEntityField()
app_name = TextEntityField(default="")
script_file_name = TextEntityField(prefixes=(app_name,))

def respond(self, message: Message) -> Reply | ReplyStream:
Expand Down
7 changes: 6 additions & 1 deletion pyttman/tools/pyttmancli/terraforming.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,15 @@ def bootstrap_app(module: str = None, devmode: bool = False,
logging_format = logging.BASIC_FORMAT

logging_handle.setFormatter(logging.Formatter(logging_format))
logger = logging.getLogger(f"Pyttman logger on app {app_name}")
logger = logging.getLogger(app_name)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging_handle)

if settings.LOG_TO_STDOUT:
stdout_handle = logging.StreamHandler(sys.stdout)
stdout_handle.setFormatter(logging.Formatter(logging_format))
logger.addHandler(stdout_handle)

# Set the configured instance of logger to the pyttman.PyttmanLogger object
pyttman.logger.LOG_INSTANCE = logger

Expand Down
2 changes: 1 addition & 1 deletion pyttman/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

__version__ = "1.3.1"
__version__ = "1.3.2"
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,22 @@ class IntentClass(ImplementedTestIntent):
cheese_type = StringEntityField(valid_strings=("blue", "yellow"),
default="blue",
as_list=True)


class PyttmanInternalTestEntityPreProcessor(
PyttmanInternalTestBaseCase
):
mock_message = Message("I would like some tea, please.")
process_message = True
expected_entities = {
"beverage": "Tea",
}

class IntentClass(ImplementedTestIntent):
"""
Tests that the 'post_processor' callable is executed and
can process the return value before it's spat out.
"""
beverage = StringEntityField(valid_strings=("tea", "coffee"),
post_processor=lambda x: x.capitalize())

0 comments on commit 3022718

Please sign in to comment.