Framework for creating web applications
Project description
Cheese Framework
Version v(1.2.14) - 22.05.08.16.44
TODO
- :bangbang: TESTS :bangbang: - 1.2.0
- do authorization - 1.3.0
- repair admin access - 1.4.0
- do Cheese tools - 1.5.0
- metadata load setting (RAM/dynamic)
- repair CORS not allowed
- remove deprecated
#@acceptsModel
annotation
Source code
https://github.com/KubaBoi/CheeseFramework/tree/development
Documentation
It is same as this BUT it is formated by my own algorithm into html. So contents is fixed on the left side of screen and it is overall clearer. (In my opinion...) Check it out :wink: it was a really hard work. All source codes are in branch webServices
in directory ./mdConverter. I would be honored if it would help you. :relieved:
https://kubaboi.github.io/CheeseFramework/
Contents
1 Introduction
Cheese Framework is open source library for creating web applications with database connection (like Spring in Java). It can save a lot of time because developer does not have to making http server or creating whole database reader. Cheese is using pydobc library for database access so it is able to connect to most of modern database engines.
:bangbang: IMPORTANT :bangbang:
Cheese Framework is using basic http python server. So DO NOT RUN outside firewall. If you are creating an application for bigger audience or you care about security use any other framework like Django and server for production like Apache or Tomcat. Stay safe :heart:
I am using Tomcat like some kind of gate which listens at 80 public port and resending requests to Cheese Applications running under firewall and listen at closed to public ports.
1.1 Instalation
1.1.1 Downloads
First of all you need to install CheeseFramework from pypi.org
pip install CheeseFramework
And you will need some more pypi packages:
pip install pyodbc
Documentationpip install psycopg2-binary
Documentationpip install psycopg2
Documentationpip install bs4
Documentationpip install requests
Documentationpip install GitPython
Documentation
1.1.2 Creating new project
Cheese has prepared template project in git branch template. I recommend to use this tool Cheeser for generating new project.
-
Open python console and import Cheeser
from Cheese.cheeser import Cheeser
-
Run generator - generator clones branch template
Cheeser.generate("path", generateFiles=True)
Path need to be full path from root and it should be empty directory or not existing directory
- for windows starts with
"C:\\..."
- for unix starts with
"/"
generateFiles parameter is default set
True
- if
False
than generator would remove HelloWorld files like ("HelloWorldController.py", "Hello.py", "helloRepository.py") - I recommend to leave it
True
when you are creating your first application so you can better understand how does it work.
- for windows starts with
That's all now you can start your application :blush:
1.1.3 Run Cheese application
Main script of Cheese applications is located in directory <your project>/src/<your project>.py
So just run this script.
Default app port is 8000
so you can check if everything works at localhost:8000
Congratulation :clap: you are now Dairy developer :clap:
2 Cheese Tools
WIP ::
3 Build
Build is proccess which generates .metadata for your application. Build runs at start of application (it is pretty fast) if is is in debug mode. Also build creates __init__.py
files inside every directory of /src/. No need to care about those files, they will be overwriten every build according to actual source code.
Building performs Cheeser.build() automatically.
4 Project structure
In this paragraph we will talk about directories and generated by Cheeser. I should tell you that there is class ResMan
in Cheese. ResMan
mediates paths to main directories of project. More about ResMan
later.
4.1 /.admin
This directory is hidden so you should not change it. But hey... you can do whatever you want. It contains web files (.html, .css, .js) for administration access. We will talk about admin access later.
4.2 /resources
Directory where belongs all non source code files which won't be served by server. Some kind of your own settings or whatever.
4.3 /src
Python source code directory. Cheese will be searching there for controllers
, models
and repositories
during building your application. You do not have to follow any structure. If .py file is in /src, it WILL be found by Cheeser.build().
4.4 /web
Directory with files served by server. Cheese server automatically looks into this directory while cannot match url endpoint with controllers.
- If url endpoint is
"/"
it search for/web/index.html
- If cannot find the file than serves
/web/errors/error404.html
- If
/web/errors/error404.html
is missing than returns ERROR 500
4.5 *.json files
Those files in root of your project are configuration files.
5 Configuration
All configration is set in *.json
files in root of project.
5.1 adminSettings.json
Contains only list of credentials for admin access.
Default adminSettings.json
{
"adminUsers":[
{
"name": "admin",
"password": "admin"
}
]
}
5.2 appSettings.json
Contains app configuration.
- name - Name of your application
- version - Version of your application
- licenseCode
- can be left empty
- it will affect if there will be
Powered by Cheese Framework
watermark at the right bottom corner of html pages served by Cheese server - if you want to get rid of this watermark go on this url
frogie.cz:6969/licence/generate?type=full%20access
and from{ "LICENSE": { "CODE": <code>, "ID": int, "TYPE": "full access" } }
copy<code>
- host - leave default
- port - port of your app
- dbDriver - driver for database
- postgres - for postgresql database
- https://github.com/mkleehammer/pyodbc/wiki - for other databases
- dbHost - host of your database
- dbName - name of your database
- dbUser - user of database
- dbPassword - password of user
- dbPort - port of your database
- allowDebug - If
false
application should be deployed. Logger will log only errors"silence"=False
logs. More about logging later. - allowCommit - If
false
application won't commit anything into database even if you write commit query - allowMultiThreading - If
true
Cheese server would be able to server more request at once (I have tested that about 60 request at once is alright) - allowCORS - If
true
Cheese server would be able to practice CORS.- Learn about CORS
- BTW... without this it does not work (yet) :scream_cat:
- allowDB - If
false
Cheese won't try to connect database
5.3 authExceptions.json
WIP
6 Annotations
Annotations are necessary for Cheese. Cheese recognize annotation that starts #@
and it needs to end with ;
. Yes it is just python comment and what?
"If it is stupid but it works, it isn't stupid"
Some Smart Guy - 1456
There is a list of all annotations:
#@controller
#@repository
#@post
#@get
#@query
#@commit
#@return
#@dbscheme
#@dbmodel
#@testclass
#@test
#@ignore
7 Python code
This will be about how to write controllers
and repositories
.
7.1 API Controllers
Controllers are classes that handle requested endpoints. I recommend to create one folder just for your controllers but it is not necessary (or use the generated one ofc).
7.1.1 Create controller
As I said, controllers are classes. So create class that inherits from CheeseController.
To let cheeser know that this class is really controller you have to anotate it with #@controller
annotation. #@controller
annotation follows part of endpoint like this:
#@controller /apiController;
class apiController(CheeseController):
7.1.2 Methods of controller
There is sctrict scheme how should method in controller looks so it can handle endpoint.
Method have to be static, so add @staticmethod
annotation above method definition.
Method have to be anotated. Annotation for endpoints contains HTTP method (right now only GET
and POST
) and endpoint.
Method have to have 3 arguments: server, path, auth.
#@post /apiEndpoint;
@staticmethod
def getFiles(server, path, auth):
7.1.3 Method arguments
Those arguments are passed by server handler. server
is instance of server handler ( BaseHTTPRequestHandler
).
path
is string variable and it contains url
without host and port. So for url http://localhost:8000/hello/world?name=helloboi
the path
will looks like this /hello/world?name=helloboi
.
auth
is some object defined by you during Authentication.
7.1.4 Example controller
Now you know almost everything you need to know to create your own controller. There are some functions that was not described yet. Those will be of course later. Controller will handle two endpoints:
/calculator/sum
/calculator/sub
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from cheese.modules.cheeseController import CheeseController
#@controller /calculator;
class CalculatorController(CheeseController):
"""
sum two numbers in request body and return result
body looks for example like this:
{
"NUM1": 1,
"NUM2": 5
}
so result will looks like this:
{
"RESPONSE": 6
}
"""
#@post /sum;
@staticmethod
def sum(server, path, auth):
#reads arguments from body of request
args = CheeseController.readArgs(server)
#arguments
num1 = int(args["NUM1"])
num2 = int(args["NUM2"])
result = num1 + num2
return CheeseController.createResponse({"RESPONSE": result}, 200)
"""
substract two numbers in request path and return result
path looks for example like this:
localhost:8000/calculator/sub?num1=5&num2=3
so result will looks like this:
{
"RESPONSE": 2
}
"""
#@get /sub;
@staticmethod
def sub(server, path, auth):
#reads arguments from endpoint path
args = CheeseController.getArgs(server)
#arguments
num1 = int(args["num1"])
num2 = int(args["num2"])
result = num1 - num2
return CheeseController.createResponse({"RESPONSE": result}, 200)
7.2 Repositories
Repository is like access into one table of database. There are methods that communicate with database.
7.2.1 Create repository
Repositories are last but most complex part of Cheese Framework. They are again classes but there need to be more annotations. Also repository have to inherits from CheeseRepository
First annotation is #@repository
, so cheeser knows this is repository, followed by name of table.
Second annotation is #@dbscheme
followed by name of table's columns in brackets where first should be Primary Key.
:bangbang: IMPORTANT :bangbang:
Column names need to be in same order as in model initializer and need to have same names!!
Last annotation is #@dbmodel
followed by name of model for the table. It is just name for your better identification of model. But it is neccessary to be there.
from cheese.modules.cheeseRepository import CheeseRepository
#@repository users;
#@dbscheme (id, user_name, age);
#@dbmodel User;
class PasswordRepository(CheeseRepository):
7.2.2 Methods of repository
Method scheme is again very strict. They need to be static and every method needs to have this line in it's body:
return CheeseRepository.query(argName1=arg1, argName2=arg2,...)
arg1
and arg2
are method's arguments.
There are two types of SQL query annotations query and commit.
#@query
This annotation says that we want to get data from database. It is followed by SQL query. This query can be more than one line but have to be in quotation marks and the query can ends with semicolon.
More line query:
#@query "select * from users
# where age > 20;";
With #@query
annotation is related annotation #@return
. It determines type of return. if annotation #@return
is missing it is default.
- DEFAULT - Returns raw data what get from database. Mostly it is tuple of tuples.
#@return raw;
- Returns array of models.
#@return array;
- Returns only one model. If there is more results returns first.
#@return one;
- Returns logical value.
#@return bool;
#@return bool
example:
#@query "select case when exists
# (select * from tokens t where t.token = :token)
# then cast(0 as bit)
# else cast(1 as bit) end;";
#@return bool;
- Returns numeric value.
#@return num;
#@return num
example:
#@query "select count(*) from users;";
#@return num;
#@commit
This annotation is for writing data into database.
#@commit "update files set id=:id where file_name=:file_name;";
You will need it only when you want to change Primary Key of some row because there are three prebuilded methods that you should add into your repository. Those methods does not have any annotation and accepts only models. The update and delete method search rows by Primary Key so if you want to update row's Primary Key you need to write your own SQL query.
7.2.3 Passing arguments to SQL query
Arguments can be insert into SQL query if you marks them with :
and in return CheeseRepository.query()
name them same as in SQL query.
Example:
#@query "select * from table where id=:someId and name=:someName;";
#@return one;
@staticmethod
def findByIdAndName(id, name):
return CheeseRepository.query(someId=id, someName=name)
7.2.3.1 Passing model
If you want to pass an model it is possible.
For model Hello
with attributes id=0
, name="first hello"
, greet="hello boi"
(this is prebuilded method save):
#@commit "insert into table values :someModel;";
@staticmethod
def save(model):
return CheeseRepository.query(someModel=model)
So the finall query which will be send to database looks like this:
insert into table values (0, 'first hello', 'hello boi');
And if you want to pass only some attribute of model do this:
#@query "select * from table where name=:someModel.name or greet=:someModel.greet;";
#@return array;
@staticmethod
def findBy(model):
return CheeseRepository.query(someModel=model)
Finall query will looks like this:
select (id, name, greet) from table where name='first hello' or greet='hello boi';
7.2.4 Prebuilded methods
There are some prebuilded methods for saving, updating, removing and find new id. You can see their settings in /.metadata/repMetadata.json
after build.
#SQL = select max(id)
@staticmethod
def findNewId():
return CheeseRepository.query()+1
#SQL = insert
@staticmethod
def save(obj):
return CheeseRepository.query(obj=obj)
#SQL = update
@staticmethod
def update(obj):
return CheeseRepository.query(obj=obj)
#SQL = delete
@staticmethod
def delete(obj):
return CheeseRepository.query(obj=obj)
8 Testing
Testing is very important part of programming. It will tell you if you fucked something up. Cheese, because of database connection (and because it is fun creating it), has it's own test system.
Tests are run during start of application after Build when application is in debug mode.
8.1 Cheese test modules
There is list of classes which your test class file should contains. Everything will be clear in the end of Testing when you check Test examples.
UnitTest
from Cheese.test import UnitTest
UnitTest
is class with methods that will throw (raise) TestError
exception which signalize to Cheese test engine that this test fails because of the result is not as expected. Those methods are static
.
Methods of UnitTest
:
UnitTest.assertEqual(value, template, comment)
If value
is not same as template
the TestError
will be raised and test fails. Also comment
will be print with fail message.
UnitTest.assertTrue(value, comment)
If value
is not equal True
the TestError
will be raised and test fails.
UnitTest.assertFalse(value, comment)
If value
is not equal False
the TestError
will be raised and test fails.
Pointer
from Cheese.pointer import Pointer
Usage of Pointer
is approximately similar as in C/C++. But because python does not have this amazing feature I had to make my own. Pointer
is just some pointer (:D) to any variable you need. I will show you that in some Test examples at the end of Testing module.
Methods of Pointer
:
Pointer.getValue()
Return value to which Pointer
points.
Pointer.setValue(value)
Sets value to which Pointer
points. (You won't need it)
Mock
from Cheese.mock import Mock
Mock
is class that you can use if you need to test some method which is using some of your repository. You can substitute return
of any method with your own values and for any argument input.
Methods of Mock
:
mock = Mock(nameOfRepository)
This is constructor (initializer) of Mock
class. Every Mock
is non-static class which mocks one repository.
mock.whenReturn(nameOfMethod, value, **kwargs)
When there is called nameOfRepository.nameOfMethod()
during test and arguments are same as **kwargs
(dictionary of arguments), then value
is returned.
mock.catchArgs(pointer, nameOfArgument, nameOfMethod, **kwargs)
When there is called nameOfRepository.nameOfMethod()
during test and arguments are same as **kwargs
, then pointer.value
will be **kwargs[nameOfArgument]
.
As I said... everything will be clear at the end of paragraph in examples.
8.2 Creating test file
Like everything else tests need to be annotated with Cheese annotations. One test file should contains only one test class (it can be more but... it is probably not best practise). This test class needs to be annotated with #@testclass
annotation. This annotation can contains some description of test class:
#@testclass this is testing something;
class helloTest:
If you add #@ignore;
annotation then whole file will be ignored during testing:
#@testclass this is testing something;
#@ignore;
class helloTest:
8.3 Test method
Test methods need #@test
annotation and also it can contain a description:
#@test I am a testing method;
@staticmethod
def helloWorldTest():
Ignored test method:
#@test I am a testing method;
#@ignore;
@staticmethod
def helloWorldTest():
8.4 Test examples
For method:
#@controller /hello;
class HelloWorldController(CheeseController):
#@get /world;
@staticmethod
def helloWorld(server, path, auth):
return "hello"
Could be test like this:
#@test hello world method;
@staticmethod
def helloWorldTest():
controller = HelloWorldController()
resp = controller.helloWorld(None, None, None)
value = resp[0].decode()
httpCode = resp[1]
UnitTest.assertEqual(value, "hello", "Response was not 'hello'")
UnitTest.assertEqual(httpCode, 200, "Status code was not 200")
For method:
#@controller /hello;
class HelloWorldController(CheeseController):
#@get /world;
@staticmethod
def helloWorld(server, path, auth):
hello = HelloRepository.model()
hello.setAttrs(hello_value="Hello boi")
return CheeseController.createResponse(hello.toJson(), 200)
Could be test like this:
#@test hello world method;
@staticmethod
def helloWorldTest():
controller = HelloWorldController()
mock = Mock("HelloRepository") # mocking HelloRepository
mock.whenReturn("findNewId", 0) # because model() method is using findNewId()
resp = controller.helloWorld(None, None, None)
value = json.loads(resp[0])
httpCode = resp[1]
# expected response should looks like this
templateResponse = {
"hello_value": "Hello boi",
"id": 1 # because findNewId() adds 1 every time
}
UnitTest.assertEqual(value, templateResponse, "Response was not as expected")
UnitTest.assertEqual(httpCode, 200, "Status code was not 200")
For method:
#@controller /hello;
class HelloWorldController(CheeseController):
#@get /world;
@staticmethod
def helloWorld(server, path, auth):
hello = hr.model()
hello.setAttrs(hello_value="Hello my friend")
hr.save(hello)
allHellos = hr.findAll()
return cc.createResponse(cc.modulesToJsonArray(allHellos), 200)
Could be test like this:
#@test hello world method;
@staticmethod
def helloWorldTest():
controller = HelloWorldController()
mock = Mock("HelloRepository")
pointer = Pointer()
mock.whenReturn("findNewId", 0) # in method model() as in previous example
"""
if method save() will be called then pointer.value will be set to value of argument "obj"
"""
mock.catchArgs(pointer, "obj", "save")
"""
if findAll() will be called then array
([pointer], that one item will be value of pointer)
will be returned
"""
mock.whenReturn("findAll", [pointer])
resp = controller.helloWorld(None, None, None)
value = json.loads(resp[0])
httpCode = resp[1]
# expected response should looks like this
templateResponse = {
"hello_value": "Hello boi",
"id": 1 # because findNewId() adds 1 every time
}
UnitTest.assertEqual(value, [templateResponse], "Response was not as expected")
UnitTest.assertEqual(httpCode, 200, "Status code was not 200")
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for CheeseFramework-1.2.14-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | d46872ebe3d9ad4e058908270b1534767c5f4028132d39a96bada5fbf43b7e29 |
|
MD5 | a6af8d6f669e4e21784fc7048ea74ae9 |
|
BLAKE2b-256 | c323c769c82cb130986d3f216deb1fafdfb2cc0553a7ff5006accb98e4b3822b |