joop 0.0.1__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.
joop/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Top-level Python package for joop."""
2
+
3
+ __author__ = """Justin Rushin"""
4
+ __version__ = '0.0.1'
5
+
6
+ def hello_world():
7
+ """Prints "Hello World!" to the console.
8
+ """
9
+ print("Hello World!")
@@ -0,0 +1,3 @@
1
+ # Abstract Utilities
2
+
3
+ This module provides utilities for abstract behavior, simplifying the creation of abstract methods and customization of dataclass behavior.
@@ -0,0 +1,95 @@
1
+ """
2
+ This module provides utilities for abstract behavior.
3
+
4
+ Classes:
5
+ AbstractMethod: A descriptor to define abstract methods in classes, particularly useful with ABCMeta and dataclasses.
6
+
7
+ Functions:
8
+ exclude_fields_from_my_init(cls, tgt_fields):
9
+ Modifies a dataclass to exclude specified fields from its `__init__` method.
10
+ """
11
+
12
+ from typing import List, Any
13
+ from dataclasses import field
14
+
15
+ class AbstractMethod:
16
+ """
17
+ A descriptor for defining abstract methods in classes.
18
+
19
+ This class is particularly useful when working with ABCMeta classes and/or dataclasses.
20
+ It allows marking methods as abstract, ensuring that they must be overridden in subclasses.
21
+
22
+ Example usage:
23
+ @classmethod
24
+ def _my_abstract_class_method(cls, something: Any):
25
+ raise NotImplementedError("Abstract; not implemented")
26
+
27
+ _my_abstract_class_method = AbstractMethod(_my_abstract_class_method)
28
+
29
+ Attributes:
30
+ func (callable): The function to be marked as abstract.
31
+ _isabstract (bool): A flag indicating whether the method is abstract.
32
+ """
33
+
34
+ def __init__(self, func):
35
+ """
36
+ Initialize the AbstractMethod descriptor.
37
+
38
+ Args:
39
+ func (callable): The function to be marked as abstract.
40
+ """
41
+ self.func = func
42
+ self._isabstract = True # Custom flag to mark as abstract
43
+
44
+ def __get__(self, instance, owner):
45
+ """
46
+ Bind the method to the class or instance.
47
+
48
+ Raises:
49
+ NotImplementedError: If the method is abstract and not overridden.
50
+
51
+ Returns:
52
+ callable: The bound method.
53
+ """
54
+ if self._isabstract:
55
+ raise NotImplementedError(f"The method {self.func.__name__} is abstract and must be overridden.")
56
+ return self.func.__get__(instance, owner)
57
+
58
+ @property
59
+ def __isabstractmethod__(self):
60
+ """
61
+ Check if the method is abstract.
62
+
63
+ Returns:
64
+ bool: True if the method is abstract, False otherwise.
65
+ """
66
+ return self._isabstract
67
+
68
+ @__isabstractmethod__.setter
69
+ def __isabstractmethod__(self, value):
70
+ """
71
+ Set the abstract status of the method.
72
+
73
+ Args:
74
+ value (bool): The new abstract status.
75
+ """
76
+ self._isabstract = value
77
+
78
+ def exclude_fields_from_my_init(cls, tgt_fields: List[str]):
79
+ """
80
+ Exclude specified fields from the `__init__` method of a dataclass.
81
+
82
+ This function is intended to be called in the `__init_subclass__` class method
83
+ of a dataclass. It ensures that the fields with names in the provided list
84
+ are marked as non-init fields.
85
+
86
+ Args:
87
+ cls (type): The dataclass to modify.
88
+ tgt_fields (List[str]): A list of field names to exclude from the `__init__` method.
89
+
90
+ """
91
+ for _field_name in tgt_fields:
92
+ current_value = getattr(cls, _field_name, None)
93
+ if _field_name not in cls.__annotations__:
94
+ cls.__annotations__[_field_name] = Any
95
+ setattr(cls, _field_name, field(init=False, default=current_value))
joop/cli/README.md ADDED
@@ -0,0 +1,6 @@
1
+ CLI featuring:
2
+ * a builtin flask webserver, for automated and manual testing. Useful for development purposes.
3
+
4
+ In an initiated shell (see main README), start with:
5
+
6
+ `python -m joop.cli --flask-server`
joop/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from joop.cli.__main__ import main
joop/cli/__main__.py ADDED
@@ -0,0 +1,39 @@
1
+ """Console script for joop development and testing purposes.
2
+
3
+ Includes:
4
+
5
+ - A hello world for the CLI.
6
+ - Start a Flask webserver for testing purposes using the `--flask-server` option.
7
+ - Display a help menu with usage instructions using the `--help` or `-h` options.
8
+
9
+ Usage:
10
+ - Run the CLI normally: `python -m joop.cli`
11
+ - Start the Flask webserver: `python -m joop.cli --flask-server`
12
+ - Display the help menu: `python -m joop.cli --help`
13
+
14
+ """
15
+ import sys
16
+ import click
17
+ from joop.cli.test_flask import start_test_flask
18
+
19
+ from joop import hello_world
20
+
21
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
22
+ @click.option('--flask-server', is_flag=True, help="Spin up a Flask webserver for testing.")
23
+ def main(flask_server, args=None):
24
+ """Console script for joop.
25
+
26
+ Usage:
27
+ - Run the CLI normally: `python -m joop.cli`
28
+ - Start the Flask webserver: `python -m joop.cli --flask-server`
29
+ - Display the help menu: `python -m joop.cli --help`
30
+ """
31
+ if flask_server:
32
+ click.echo("Starting Flask webserver...")
33
+ start_test_flask()
34
+ else:
35
+ click.echo(hello_world())
36
+ return 0
37
+
38
+ if __name__ == "__main__":
39
+ main()
joop/cli/test_flask.py ADDED
@@ -0,0 +1,36 @@
1
+ """Flask testing module for joop.
2
+
3
+ This module provides functionality to start a Flask webserver for testing and development purposes.
4
+
5
+ Important: The test joop env is set as the jinja env for the Flask server. This is specific and desirable
6
+ though your project may implement the environment otherwise.
7
+
8
+ Functions:
9
+ start_test_flask():
10
+ Starts the Flask webserver in debug mode using the `app` instance from `joop.flask`.
11
+
12
+ Dependencies:
13
+ - Flask: Ensure Flask is installed. If not, install it using `pip install joop[flask]`.
14
+
15
+ Usage:
16
+ - Import the `start_test_flask` function and call it to start the Flask webserver.
17
+
18
+ """
19
+
20
+ from joop.tests.test_templater import environment
21
+ try:
22
+ from joop.flask import app
23
+
24
+ joop_env = environment
25
+
26
+
27
+ def start_test_flask():
28
+ global app
29
+
30
+ app.run(debug=True)
31
+
32
+ except ImportError as e:
33
+ raise ImportError(
34
+ "The 'flask' module requires additional dependencies. "
35
+ "Install them using 'pip install joop[flask]'."
36
+ )
joop/dao/__init__.py ADDED
@@ -0,0 +1,151 @@
1
+ """Data Access Object (DAO) module for joop.
2
+
3
+ This module provides abstract classes for creating Data Access Objects (DAOs) to manage
4
+ Pydantic and SQLModel-based models. DAOs provide a layer of abstraction between the
5
+ representation of the object (especially at the storage layer) and the business logic
6
+ code that uses or manipulates the object.
7
+
8
+ Classes:
9
+ DAO:
10
+ An abstract wrapper for a Pydantic model or any class derived from it.
11
+
12
+ SQLDAO:
13
+ An abstract class for SQL models, extending the DAO class.
14
+
15
+ """
16
+
17
+ from typing import List, Type, Optional
18
+ from dataclasses import dataclass
19
+ import pydantic
20
+ import sqlmodel
21
+
22
+ class DAO():
23
+ """
24
+ An abstract wrapper for a Pydantic model or any class derived from it.
25
+
26
+ The DAO class provides methods to interact with the underlying model, including
27
+ converting it to a dictionary and retrieving its fields. It is typically constructed
28
+ around an existing model using the `from_model` method.
29
+
30
+ Attributes:
31
+ _modeltype (Type): The type of the model, defaulting to `pydantic.BaseModel`.
32
+
33
+ Properties:
34
+ model (pydantic.BaseModel): The underlying Pydantic model instance.
35
+
36
+ Methods:
37
+ from_model(model: pydantic.BaseModel) -> 'DAO':
38
+ Creates a DAO instance from a given Pydantic model.
39
+
40
+ to_dict() -> dict:
41
+ Converts the underlying model to a dictionary using aliases for field names.
42
+
43
+ get_model_fields() -> List[str]:
44
+ Retrieves the names of all fields defined in the `_modeltype`.
45
+ """
46
+
47
+ _modeltype : Type = pydantic.BaseModel
48
+
49
+ @property
50
+ def model(self) -> pydantic.BaseModel:
51
+ return self._model
52
+
53
+ @model.setter
54
+ def model(self, value : pydantic.BaseModel):
55
+ self._model = value
56
+
57
+ @classmethod
58
+ def from_model(cls, model : pydantic.BaseModel) -> 'DAO':
59
+ if model is None:
60
+ return None
61
+
62
+ if isinstance(type(model), cls._modeltype):
63
+ raise ("Invalid Model Supplied")
64
+
65
+ res = cls()
66
+ res._model = model
67
+ return res
68
+
69
+ def to_dict(self) -> dict:
70
+ """
71
+ Convert the underlying model to a dictionary, using aliases for field names.
72
+
73
+ Returns:
74
+ dict: A dictionary representation of the model with aliases as keys.
75
+
76
+ Raises:
77
+ ValueError: If no model is set for this DAO instance.
78
+ TypeError: If the model does not have Pydantic fields.
79
+ """
80
+ if not hasattr(self, '_model') or self._model is None:
81
+ raise ValueError("No model is set for this DAO instance.")
82
+
83
+ # Ensure the model has the necessary fields and aliases
84
+ if not hasattr(self._model, '__fields__'):
85
+ raise TypeError("The model does not have Pydantic fields.")
86
+
87
+ # Use SQLModel's/Pydantic basemodel's dict() method to get the base dictionary
88
+ base_dict = self._model.dict()
89
+
90
+ # Build the dictionary using aliases
91
+ result = {}
92
+ for field_name, field in self._model.__fields__.items():
93
+ alias = field.alias or field_name # Use alias if defined, otherwise fallback to field name
94
+ result[alias] = base_dict[field_name]
95
+
96
+ return result
97
+
98
+ @classmethod
99
+ def get_model_fields(cls) -> List[str]:
100
+ """
101
+ Get the names of all fields defined in the `_modeltype`.
102
+
103
+ Returns:
104
+ List[str]: A list of field names.
105
+
106
+ Raises:
107
+ TypeError: If `_modeltype` is not a subclass of `pydantic.BaseModel`.
108
+ """
109
+ if not issubclass(cls._modeltype, pydantic.BaseModel):
110
+ raise TypeError("_modeltype must be a subclass of pydantic.BaseModel")
111
+
112
+ res = [field.alias if field.alias else name for name, field in cls._modeltype.__fields__.items()]
113
+ return res
114
+
115
+ class SQLDAO(DAO):
116
+ """
117
+ An abstract class for SQL models, extending the DAO class.
118
+
119
+ The SQLDAO class provides additional methods for interacting with SQLModel-based
120
+ models, such as retrieving all records from the database.
121
+
122
+ Attributes:
123
+ _modeltype (Type): The type of the model, defaulting to `sqlmodel.SQLModel`.
124
+
125
+ Methods:
126
+ get_all(session: sqlmodel.Session) -> List['SQLDAO']:
127
+ Retrieves all records from the database for the given model type and returns
128
+ them as a list of SQLDAO instances.
129
+ """
130
+
131
+ _modeltype : Type = sqlmodel.SQLModel
132
+
133
+ @classmethod
134
+ def get_all(cls, session: sqlmodel.Session) -> List['SQLDAO']:
135
+ """
136
+ Retrieve all records from the database for the given model type and return them as a list of SQLDAO instances.
137
+
138
+ Args:
139
+ session (sqlmodel.Session): The database session to use for the query.
140
+
141
+ Returns:
142
+ List[SQLDAO]: A list of SQLDAO instances for the model type.
143
+
144
+ Raises:
145
+ TypeError: If `_modeltype` is not a subclass of `sqlmodel.SQLModel`.
146
+ """
147
+ if not issubclass(cls._modeltype, sqlmodel.SQLModel):
148
+ raise TypeError("_modeltype must be a subclass of sqlmodel.SQLModel")
149
+
150
+ db_results = session.query(cls._modeltype).all()
151
+ return [cls.from_model(result) for result in db_results]
joop/flask/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Flask application initialization module for joop.
2
+
3
+ This is used for example, development, and testing purposes.
4
+ This module initializes a Flask application and configures it with the joop Jinja2 environment.
5
+ That is important. ^^^ We are not using the default Jinja2 env, and that is intentional.
6
+ This is the place to register example Flask views to the application.
7
+
8
+ Attributes:
9
+ app (Flask): The Flask application instance.
10
+
11
+ Modules:
12
+ """
13
+
14
+ from __future__ import absolute_import
15
+ from flask import Flask
16
+ from joop.web.j_env import joop_env
17
+ from joop.flask.example import (
18
+ FlaskHello, FlaskName, FlaskTablePage
19
+ )
20
+
21
+ app = Flask(__name__)
22
+
23
+ # Configure the Jinja2 environment for the Flask application
24
+ # Important to do this and not use the default.
25
+ app.jinja_env = joop_env
26
+
27
+ # Register example views to the Flask application
28
+ # ex. MyView.add_to_app(app)
29
+ FlaskHello.add_to_app(app)
30
+ FlaskName.add_to_app(app)
31
+ FlaskTablePage.add_to_app(app)
joop/flask/example.py ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+
3
+ This is the default place to turn example or other View definitions into
4
+ flask-specific views.
5
+ """
6
+
7
+ from joop.web.examples.view import (
8
+ NameView, HelloView
9
+ )
10
+
11
+ from joop.web.examples.table import MyTableWholePage
12
+
13
+ from joop.flask.flask_view import FlaskView
14
+
15
+ class FlaskHello(HelloView, FlaskView): pass
16
+
17
+ class FlaskName(NameView, FlaskView): pass
18
+
19
+ class FlaskTablePage(MyTableWholePage, FlaskView): pass
@@ -0,0 +1,71 @@
1
+ """Flask view integration for joop.
2
+
3
+ This module provides a base class for integrating joop views with Flask applications.
4
+
5
+ Any regular joop View can do multiple inheritance with a FlaskView to result
6
+ in a class that can be used with Flask.
7
+
8
+ Functionality include jinja env proxying.
9
+
10
+ Classes:
11
+ FlaskView:
12
+ A base class for creating Flask-compatible views from joop View classes.
13
+ """
14
+
15
+ from typing import Optional
16
+ from flask import Flask, current_app
17
+
18
+ from joop.web.view import View, Component
19
+
20
+ class FlaskView(View):
21
+ """
22
+ A base class for creating Flask-compatible views from joop View classes.
23
+
24
+ This class provides methods to integrate joop views with Flask applications by
25
+ adding URL rules and accessing the Flask Jinja2 environment.
26
+
27
+ Use multiple inheritance to make your View a FlaskView ex.
28
+
29
+ `class FlaskHello(HelloView, FlaskView):`
30
+
31
+
32
+ Methods:
33
+ _add_to_app(app: Flask, view_func: callable):
34
+ Adds the view to a Flask application with the specified view function.
35
+
36
+ _get_jinja_env():
37
+ Retrieves the Jinja2 environment from the current Flask application context.
38
+ """
39
+
40
+ @classmethod
41
+ def _add_to_app(cls, app: Flask, view_func: callable):
42
+ """
43
+ Add the view to a Flask application with the specified view function.
44
+
45
+ Args:
46
+ app (Flask): The Flask application instance.
47
+ view_func (callable): The view function to associate with the URL rule.
48
+
49
+ This method registers a URL rule with the Flask application, associating it
50
+ with the specified view function and HTTP methods defined in the Endpoint.
51
+ """
52
+ app.add_url_rule(
53
+ rule=cls.Endpoint._url, # URL rule as a string
54
+ endpoint=cls.Endpoint._name, # Endpoint name
55
+ view_func=view_func, # The view function to call
56
+ methods=cls.Endpoint._methods # List of HTTP methods allowed
57
+ )
58
+
59
+ @classmethod
60
+ def _get_jinja_env(cls):
61
+ """
62
+ Retrieve the Jinja2 environment from the current Flask application context.
63
+
64
+ Returns:
65
+ jinja2.Environment: The Jinja2 environment associated with the current
66
+ Flask application context.
67
+
68
+ This method allows joop views to access the Jinja2 environment configured
69
+ for the Flask application.
70
+ """
71
+ return current_app.jinja_env
joop/http/methods.py ADDED
@@ -0,0 +1,49 @@
1
+ """HTTP Methods Enumeration Module.
2
+
3
+ joop uses its own HTTP method enum implementation for compatibility purposes with
4
+ older versions of python, to provide a standardized way to
5
+ represent and describe HTTP methods.
6
+
7
+ Classes:
8
+ HttpMethod:
9
+ An enumeration of HTTP methods, each with a corresponding description.
10
+
11
+ Methods:
12
+ description() -> str:
13
+ Returns a description of the HTTP method.
14
+
15
+ Usage:
16
+ - Use `HttpMethod` to refer to HTTP methods in a type-safe manner.
17
+ - Call the `description` method on an `HttpMethod` instance to get its description.
18
+
19
+ """
20
+
21
+ from enum import Enum
22
+
23
+ class HttpMethod(str, Enum): # Changed from StrEnum to str + Enum for Python 3.10 compatibility
24
+ """Enumeration of HTTP methods with their descriptions."""
25
+
26
+ GET = "GET"
27
+ POST = "POST"
28
+ PUT = "PUT"
29
+ DELETE = "DELETE"
30
+ PATCH = "PATCH"
31
+ OPTIONS = "OPTIONS"
32
+ HEAD = "HEAD"
33
+ TRACE = "TRACE"
34
+ CONNECT = "CONNECT"
35
+
36
+ def description(self) -> str:
37
+ """Return a description of the HTTP method."""
38
+ descriptions = {
39
+ "GET": "Retrieve data from the server.",
40
+ "POST": "Submit data to the server.",
41
+ "PUT": "Update or create a resource on the server.",
42
+ "DELETE": "Delete a resource on the server.",
43
+ "PATCH": "Apply partial modifications to a resource.",
44
+ "OPTIONS": "Describe the communication options for the target resource.",
45
+ "HEAD": "Retrieve metadata about the resource without the body.",
46
+ "TRACE": "Perform a message loop-back test along the path to the target resource.",
47
+ "CONNECT": "Establish a tunnel to the server identified by the target resource."
48
+ }
49
+ return descriptions[self.value]
joop/tests/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Test suite catalog."""
2
+
3
+ # from joop.tests.test_joop import TestJoop
4
+ from joop.tests.test_web import TestHTMLComponent
5
+ from joop.tests.test_view import TestView
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python
2
+
3
+ """High-level test suite. Currently, verifies that the CLI works.."""
4
+
5
+
6
+ import unittest
7
+ from click.testing import CliRunner
8
+
9
+ from joop.cli import main
10
+
11
+
12
+ class TestJoop(unittest.TestCase):
13
+ """Tests for `joop` package."""
14
+
15
+ def setUp(self):
16
+ """Set up test fixtures, if any."""
17
+
18
+ def tearDown(self):
19
+ """Tear down test fixtures, if any."""
20
+
21
+ def test_001_command_line_interface(self):
22
+ """Test the CLI."""
23
+ runner = CliRunner()
24
+ result = runner.invoke(main)
25
+ assert result.exit_code == 0
26
+ assert 'Hello World' in result.output
27
+ help_result = runner.invoke(main, ['--help'])
28
+ assert help_result.exit_code == 0
29
+ assert '--help Show this message and exit.' in help_result.output
@@ -0,0 +1,31 @@
1
+ """Templater testing module for joop.
2
+
3
+ This module instantiates a jinja templating engine for development and testing purposes.
4
+ It's not just restricted to automated testing.
5
+
6
+ Classes:
7
+ BaseTestHTMLComponent:
8
+ A base class for testing HTML components.
9
+
10
+ Variables:
11
+ environment:
12
+ A placeholder variable for the templating environment.
13
+
14
+ Usage:
15
+ - Utilize the `environment` variable for templating-related operations.
16
+
17
+ """
18
+
19
+ from joop.web.templater import EnvironmentFactory
20
+ from jinja2 import FileSystemLoader
21
+ from joop.web.j_env import set_joop_env
22
+
23
+ from pathlib import Path
24
+
25
+ WEB_TEMPLATES_ROOT = Path("../templates/examples/")
26
+
27
+ environment = EnvironmentFactory.create_environment(
28
+ loader=FileSystemLoader(WEB_TEMPLATES_ROOT)
29
+ )
30
+
31
+ set_joop_env(environment)
@@ -0,0 +1,46 @@
1
+ """Unit tests for joop Flask integration.
2
+
3
+ This module contains unit tests for the Flask views in the joop project. It uses the
4
+ unittest framework and Flask's test_client to test the functionality of the views.
5
+
6
+ It borders on being an integration test, insofar as we are testing:
7
+ * joop's views
8
+ * joop's templater
9
+ * joop's flask integration
10
+
11
+ Classes:
12
+ TestView:
13
+ Test cases for the Flask views in the joop application.
14
+
15
+ """
16
+
17
+ import unittest
18
+
19
+ try:
20
+ from joop.flask import app
21
+ APP_AVAILABLE = app is not None
22
+ except ImportError:
23
+ APP_AVAILABLE = False
24
+
25
+ @unittest.skipIf(not APP_AVAILABLE, "joop.flask or app is not available")
26
+ class TestView(unittest.TestCase):
27
+ """
28
+ Test cases for the Flask views in the joop application.
29
+
30
+ This test class uses the Flask test client to send requests to the application
31
+ and verify the responses.
32
+
33
+ Methods:
34
+ setUp():
35
+ Sets up the test client for the Flask application.
36
+
37
+ test_000_hello():
38
+ Tests the '/hello' endpoint to ensure it returns a 200 status code.
39
+ """
40
+
41
+ def setUp(self):
42
+ self.client = app.test_client()
43
+
44
+ def test_000_hello(self):
45
+ response = self.client.get('/hello')
46
+ self.assertEqual(response.status_code, 200)