Skip to main content

Framework for creating web applications

Project description

Cheese Framework

Release Build

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:

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.

  1. Open python console and import Cheeser

    from Cheese.cheeser import Cheeser
    
  2. 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.

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
  • 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.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

CheeseFramework-1.2.14.tar.gz (44.6 kB view hashes)

Uploaded Source

Built Distribution

CheeseFramework-1.2.14-py3-none-any.whl (46.0 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page